with ada.text_io;
with ada.integer_text_io;
with ada.numerics.discrete_random;

procedure lab8 is

    subtype boardSide is Integer range 1..4;
    subtype pieceRange is Integer range 1..15;

    type boardPiece is
	record
	    x, y : boardSide;
	    value : pieceRange;
	    empty : Boolean;
	end record;
    
    type boardArray is array(boardSide, boardSide) of boardPiece;

    
    package t_io renames ada.text_io;
    package i_io renames ada.integer_text_io;
    package randomPiece is new ada.numerics.discrete_random(boardSide);

    procedure initBoard;
    procedure printBoard;
    procedure swapPiece(boardNumber : in pieceRange);
    function findPiece(boardNumber : in pieceRange) return boardPiece;
    function isSlidable(thisPiece : boardPiece) return Boolean;
    function isComplete return Boolean;

    board : boardArray;
    nextPiece : pieceRange;
    empty : boardPiece;
    G : randomPiece.generator;

    not_slidable_error : exception;
    piece_not_found : exception;

    procedure initBoard is
	side : boardSide;
    begin

	for j in boardSide loop
	    for i in boardSide loop
		board(i, j).x := i;
		board(i, j).y := j;
		if i = boardSide'last and j = boardSide'last then
		    board(i, j).empty := true;
		    empty := board(i, j);
		else
		    board(i, j).empty := false;
		    board(i, j).value := ((j-1) * boardSide'last) + i;
		end if;
	    end loop;
	end loop;

	for r in 1..250 loop
	    side := randomPiece.random(G);
	    for i in 1..4 loop
		 begin
		    CASE SIDE IS
			WHEN 1 =>
			    IF EMPTY.X > 1 THEN
				SWAPpIECE( BOARD(EMPTY.X-1, EMPTY.Y).VALUE );
				exit;
			    else
				side := 2;
			    END IF;
			WHEN 2 =>
			    IF EMPTY.X < BOARDsIDE'LAST THEN
				SWAPpIECE( BOARD(EMPTY.X+1, EMPTY.Y).VALUE );
				exit;
			    else
				side := 3;
			    END IF;
			WHEN 3 =>
			    IF EMPTY.Y > 1 THEN
				SWAPpIECE( BOARD(EMPTY.X, EMPTY.Y-1).VALUE );
				exit;
			    else
				side := 4;
			    END IF;
			WHEN 4 =>
			    IF EMPTY.Y < BOARDsIDE'LAST THEN
				SWAPpIECE( BOARD(EMPTY.X, EMPTY.Y+1).VALUE );
				exit;
			    else
				side := 1;
			    END IF;
		    END CASE;
		exception
		    when not_slidable_error =>
		       null;
	       end;
	    end loop;
	end loop;

    end initBoard;

    procedure printBoard is
    begin
	for j in boardSide loop
	    for i in boardSide loop
		if i = empty.x and j = empty.y then
		    t_io.put("    ");
		else
		    i_io.put(item => board(i, j).value, width => 4);
		end if;
	    end loop;
	    t_io.new_line;
	end loop;
    end printBoard;

    procedure swapPiece(boardNumber : in pieceRange) is
	thisPiece, tmp : boardPiece;
    begin
	thisPiece := findPiece(boardNumber);

	if isSlidable(thisPiece) = false then
	    raise not_slidable_error;
	end if;
	
	board(empty.x, empty.y).value := thisPiece.value;
	board(empty.x, empty.y).empty := false;
	board(thisPiece.x, thisPiece.y).empty := true;
	empty.x := thisPiece.x;
	empty.y := thisPiece.y;

    end swapPiece;

    function findPiece(boardNumber: in pieceRange) return boardPiece is
    begin
	for j in boardSide loop
	    for i in boardSide loop
		if board(i, j).value = boardNumber then
		    return board(i, j);
		end if;
	    end loop;
	end loop;
	raise piece_not_found;
    end findPiece;

    function isSlidable (thisPiece : boardPiece) return Boolean is
    begin
	
	if thisPiece.x > 1 then 
	    if board(thisPiece.x -1, thisPiece.y).empty = true then
		return true;
	    end if;
	end if;
	if thisPiece.x < boardSide'last then 
	    if board(thisPiece.x +1, thisPiece.y).empty = true then
		return true;
	    end if;
	end if;
	if thisPiece.y > 1 then 
	    if board(thisPiece.x, thisPiece.y -1).empty = true then
		return true;
	    end if;
	end if;
	if thisPiece.y < boardSide'last then 
	    if board(thisPiece.x, thisPiece.y +1).empty = true then
		return true;
	    end if;
	end if;
	return false;
    end isSlidable;

    function isComplete return Boolean is
    begin
	for j in boardSide loop
	    for i in boardSide loop
		if i = boardSide'last and j = boardSide'last then
		    return true;
		end if;
		if board(i,j).value /= ((j-1)*boardSide'last + i) then
		    return false;
		end if;
	    end loop;
	end loop;
	return true;
    end isComplete;

    
begin

    randomPiece.reset(G);
    initBoard;
    loop
	begin
	    printBoard;
	    t_io.put("Enter the number of the piece you want to move (between 1 and 15)  > ");
	    i_io.get(nextPiece);
	    swapPiece(nextPiece);
	    exit when isComplete;
	exception
	    when constraint_error | t_io.data_error | not_slidable_error =>
		t_io.skip_line;
		t_io.new_line;
		t_io.put_line("Invalid piece, make sure the piece you select is adjacent to the empty square");
		t_io.new_line;
	end;
    end loop;

end lab8;

